package org.jflame.commons.util;

import org.jflame.commons.crypto.digest.DigestHelper;
import org.jflame.commons.crypto.digest.Signer;
import org.jflame.commons.crypto.digest.Hmac;
import org.jflame.commons.crypto.digest.HmacAlg;
import org.jflame.commons.crypto.digest.SignatureAlg;
import org.jflame.commons.model.Chars;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.security.DigestException;
import java.security.SignatureException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

public class SignHelper {

    /**
     * 使用摘要算法签名,签名规则:以key的ASCII码升序,以形式{@code key1=value1&key2=value2... } 拼接成字符串,然后使用指定的摘要签名算法生成签名,以base64编码串返回
     * 
     * @param algorithm 算法名SignatureAlgorithm
     * @param privateKey 私钥
     * @param mapData
     * @return
     * @throws SignatureException
     */
    public static String sign(SignatureAlg algorithm, String privateKey, Map<String,?> mapData)
            throws SignatureException {
        if (MapHelper.isEmpty(mapData)) {
            return null;
        }
        String content;
        if (mapData instanceof TreeMap) {
            content = StringHelper.buildUrlParamFromMap(mapData);
        } else {
            content = StringHelper.buildUrlParamFromMap(new TreeMap<>(mapData));
        }

        return Signer.sign(algorithm, content, privateKey);
    }

    /**
     * 使用HMAC摘要算法计算签名
     * 
     * @param algorithm
     * @param privateKey
     * @param mapData
     * @return
     * @throws DigestException
     */
    public static byte[] hmacSign(HmacAlg algorithm, String privateKey, Map<String,?> mapData)
            throws SignatureException {
        if (MapHelper.isEmpty(mapData)) {
            return null;
        }
        String content;
        if (mapData instanceof TreeMap) {
            content = StringHelper.buildUrlParamFromMap(mapData);
        } else {
            content = StringHelper.buildUrlParamFromMap(new TreeMap<>(mapData));
        }
        try {
            return Hmac.hmac(algorithm, CharsetHelper.getUtf8Bytes(content), CharsetHelper.getUtf8Bytes(privateKey));
        } catch (DigestException e) {
            throw new SignatureException(e);
        }
    }

    /**
     * 验证数字签名,签名规则:以key的ASCII码升序,以形式{@code key1=value1&key2=value2... } 拼接成字符串,然后使用指定的摘要签名算法生成签名
     * 
     * @param algorithm 算法名SignatureAlgorithm
     * @param publicKey 私钥
     * @param mapData
     * @param signBase64Str 原签名
     * @return
     */
    public static boolean verify(SignatureAlg algorithm, String publicKey, Map<String,?> mapData,
            String signBase64Str) {
        String content;
        if (mapData instanceof TreeMap) {
            content = StringHelper.buildUrlParamFromMap(mapData);
        } else {
            content = StringHelper.buildUrlParamFromMap(new TreeMap<>(mapData));
        }
        return Signer.verify(algorithm, content, signBase64Str, publicKey);
    }

    public static String mapParamSign(Map<String,?> mapData) throws SignatureException {
        return mapParamSign(mapData, null);
    }

    /**
     * <p>
     * map参数签名,签名算法: 以key按单词自然排序后,将key与value用=和&amp;拼接, 再对字符串做md5,null值忽略.<br>
     * 如:{@code md5hex(k1=v1&k2=v2&)}
     * </p>
     * <p>
     * 注:时间格式(Date,LocalDateTime)默认格式:yyyyMMddHHmmss,日期格式(LocalDate,java.sql.Date):yyyyMMdd
     * 
     * @param mapData 待签名的map
     * @param excludeKeys 不参与签名的map key
     * @return
     * @throws DigestException
     */
    @SuppressWarnings("unchecked")
    public static String mapParamSign(Map<String,?> mapData, String[] excludeKeys) throws SignatureException {
        if (MapHelper.isEmpty(mapData)) {
            return null;
        }
        StringBuilder str = new StringBuilder(30);
        SortedMap<String,Object> sortedMap;
        if (mapData instanceof TreeMap) {
            sortedMap = (TreeMap<String,Object>) mapData;
        } else {
            sortedMap = new TreeMap<>(mapData);
        }

        boolean hasExclude = ArrayUtils.isNotEmpty(excludeKeys);
        boolean isArray = false;
        for (Entry<String,?> kv : sortedMap.entrySet()) {
            if (hasExclude && ArrayUtils.contains(excludeKeys, kv.getKey())) {
                continue;
            }
            // 值为null忽略
            if (kv.getValue() == null || StringUtils.EMPTY.equals(kv.getValue())) {
                continue;
            }
            // 数组长度为0忽略
            isArray = kv.getValue()
                    .getClass()
                    .isArray();
            if (isArray && Array.getLength(kv.getValue()) == 0) {
                continue;
            }
            str.append(Chars.AND);
            str.append(kv.getKey())
                    .append(Chars.EQUAL);
            if (kv.getValue() instanceof BigDecimal) {
                str.append(((BigDecimal) kv.getValue()).stripTrailingZeros()
                        .toPlainString());
            }
            if (kv.getValue() instanceof Date) {
                str.append(DateHelper.format((Date) kv.getValue(), DateHelper.yyyyMMddHHmmss));
            } else if (kv.getValue() instanceof LocalDateTime) {
                str.append(DateHelper.format((LocalDateTime) kv.getValue(), DateHelper.yyyyMMddHHmmss));
            } else if (kv.getValue() instanceof LocalDate) {
                str.append(DateHelper.format((LocalDate) kv.getValue(), DateHelper.yyyyMMdd));
            } else if (kv.getValue() instanceof java.sql.Date) {
                str.append(DateHelper.format((java.sql.Date) kv.getValue(), DateHelper.yyyyMMdd));
            } else if (isArray) {
                str.append(toArrayString(kv.getValue()));
            } else {
                str.append(kv.getValue());
            }
        }

        try {
            return DigestHelper.md5Hex(str.substring(1));
        } catch (DigestException e) {
            throw new SignatureException(e);
        }
    }

    /**
     * 未知元素类型的数组toString.字符串组成同Arrays.toString()
     *
     * @param a 数组
     * @return
     */
    private static String toArrayString(Object a) {
        if (a == null)
            return "null";
        int iMax = Array.getLength(a) - 1;
        if (iMax == -1)
            return "[]";

        StringBuilder b = new StringBuilder();
        b.append('[');
        for (int i = 0;; i++) {
            b.append(Array.get(a, i));
            if (i == iMax)
                return b.append(']')
                        .toString();
            b.append(", ");
        }
    }

}
